// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/x/selection_owner.h"

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <algorithm>

#include "base/logging.h"
#include "ui/base/x/selection_utils.h"
#include "ui/base/x/x11_foreign_window_manager.h"
#include "ui/base/x/x11_util.h"

namespace ui {

namespace {

    const char kAtomPair[] = "ATOM_PAIR";
    const char kIncr[] = "INCR";
    const char kMultiple[] = "MULTIPLE";
    const char kSaveTargets[] = "SAVE_TARGETS";
    const char kTargets[] = "TARGETS";

    const char* kAtomsToCache[] = {
        kAtomPair,
        kIncr,
        kMultiple,
        kSaveTargets,
        kTargets,
        NULL
    };

    // The period of |incremental_transfer_abort_timer_|. Arbitrary but must be <=
    // than kIncrementalTransferTimeoutMs.
    const int kTimerPeriodMs = 1000;

    // The amount of time to wait for the selection requestor to process the data
    // sent by the selection owner before aborting an incremental data transfer.
    const int kIncrementalTransferTimeoutMs = 10000;

    static_assert(kTimerPeriodMs <= kIncrementalTransferTimeoutMs,
        "timer period must be <= transfer timeout");

    // Returns a conservative max size of the data we can pass into
    // XChangeProperty(). Copied from GTK.
    size_t GetMaxRequestSize(XDisplay* display)
    {
        long extended_max_size = XExtendedMaxRequestSize(display);
        long max_size = (extended_max_size ? extended_max_size : XMaxRequestSize(display)) - 100;
        return std::min(static_cast<long>(0x40000),
            std::max(static_cast<long>(0), max_size));
    }

    // Gets the value of an atom pair array property. On success, true is returned
    // and the value is stored in |value|.
    bool GetAtomPairArrayProperty(XID window,
        XAtom property,
        std::vector<std::pair<XAtom, XAtom>>* value)
    {
        XAtom type = None;
        int format = 0; // size in bits of each item in 'property'
        unsigned long num_items = 0;
        unsigned char* properties = NULL;
        unsigned long remaining_bytes = 0;

        int result = XGetWindowProperty(gfx::GetXDisplay(),
            window,
            property,
            0, // offset into property data to
            // read
            (~0L), // entire array
            False, // deleted
            AnyPropertyType,
            &type,
            &format,
            &num_items,
            &remaining_bytes,
            &properties);
        gfx::XScopedPtr<unsigned char> scoped_properties(properties);

        if (result != Success)
            return false;

        // GTK does not require |type| to be kAtomPair.
        if (format != 32 || num_items % 2 != 0)
            return false;

        XAtom* atom_properties = reinterpret_cast<XAtom*>(properties);
        value->clear();
        for (size_t i = 0; i < num_items; i += 2)
            value->push_back(std::make_pair(atom_properties[i], atom_properties[i + 1]));
        return true;
    }

} // namespace

SelectionOwner::SelectionOwner(XDisplay* x_display,
    XID x_window,
    XAtom selection_name)
    : x_display_(x_display)
    , x_window_(x_window)
    , selection_name_(selection_name)
    , max_request_size_(GetMaxRequestSize(x_display))
    , atom_cache_(x_display_, kAtomsToCache)
{
}

SelectionOwner::~SelectionOwner()
{
    // If we are the selection owner, we need to release the selection so we
    // don't receive further events. However, we don't call ClearSelectionOwner()
    // because we don't want to do this indiscriminately.
    if (XGetSelectionOwner(x_display_, selection_name_) == x_window_)
        XSetSelectionOwner(x_display_, selection_name_, None, CurrentTime);
}

void SelectionOwner::RetrieveTargets(std::vector<XAtom>* targets)
{
    for (SelectionFormatMap::const_iterator it = format_map_.begin();
         it != format_map_.end(); ++it) {
        targets->push_back(it->first);
    }
}

void SelectionOwner::TakeOwnershipOfSelection(
    const SelectionFormatMap& data)
{
    XSetSelectionOwner(x_display_, selection_name_, x_window_, CurrentTime);

    if (XGetSelectionOwner(x_display_, selection_name_) == x_window_) {
        // The X server agrees that we are the selection owner. Commit our data.
        format_map_ = data;
    }
}

void SelectionOwner::ClearSelectionOwner()
{
    XSetSelectionOwner(x_display_, selection_name_, None, CurrentTime);
    format_map_ = SelectionFormatMap();
}

void SelectionOwner::OnSelectionRequest(const XEvent& event)
{
    XID requestor = event.xselectionrequest.requestor;
    XAtom requested_target = event.xselectionrequest.target;
    XAtom requested_property = event.xselectionrequest.property;

    // Incrementally build our selection. By default this is a refusal, and we'll
    // override the parts indicating success in the different cases.
    XEvent reply;
    reply.xselection.type = SelectionNotify;
    reply.xselection.requestor = requestor;
    reply.xselection.selection = event.xselectionrequest.selection;
    reply.xselection.target = requested_target;
    reply.xselection.property = None; // Indicates failure
    reply.xselection.time = event.xselectionrequest.time;

    if (requested_target == atom_cache_.GetAtom(kMultiple)) {
        // The contents of |requested_property| should be a list of
        // <target,property> pairs.
        std::vector<std::pair<XAtom, XAtom>> conversions;
        if (GetAtomPairArrayProperty(requestor,
                requested_property,
                &conversions)) {
            std::vector<XAtom> conversion_results;
            for (size_t i = 0; i < conversions.size(); ++i) {
                bool conversion_successful = ProcessTarget(conversions[i].first,
                    requestor,
                    conversions[i].second);
                conversion_results.push_back(conversions[i].first);
                conversion_results.push_back(
                    conversion_successful ? conversions[i].second : None);
            }

            // Set the property to indicate which conversions succeeded. This matches
            // what GTK does.
            XChangeProperty(
                x_display_,
                requestor,
                requested_property,
                atom_cache_.GetAtom(kAtomPair),
                32,
                PropModeReplace,
                reinterpret_cast<const unsigned char*>(&conversion_results.front()),
                conversion_results.size());

            reply.xselection.property = requested_property;
        }
    } else {
        if (ProcessTarget(requested_target, requestor, requested_property))
            reply.xselection.property = requested_property;
    }

    // Send off the reply.
    XSendEvent(x_display_, requestor, False, 0, &reply);
}

void SelectionOwner::OnSelectionClear(const XEvent& event)
{
    DLOG(ERROR) << "SelectionClear";

    // TODO(erg): If we receive a SelectionClear event while we're handling data,
    // we need to delay clearing.
}

bool SelectionOwner::CanDispatchPropertyEvent(const XEvent& event)
{
    return event.xproperty.state == PropertyDelete && FindIncrementalTransferForEvent(event) != incremental_transfers_.end();
}

void SelectionOwner::OnPropertyEvent(const XEvent& event)
{
    std::vector<IncrementalTransfer>::iterator it = FindIncrementalTransferForEvent(event);
    if (it == incremental_transfers_.end())
        return;

    ProcessIncrementalTransfer(&(*it));
    if (!it->data.get())
        CompleteIncrementalTransfer(it);
}

bool SelectionOwner::ProcessTarget(XAtom target,
    XID requestor,
    XAtom property)
{
    XAtom multiple_atom = atom_cache_.GetAtom(kMultiple);
    XAtom save_targets_atom = atom_cache_.GetAtom(kSaveTargets);
    XAtom targets_atom = atom_cache_.GetAtom(kTargets);

    if (target == multiple_atom || target == save_targets_atom)
        return false;

    if (target == targets_atom) {
        // We have been asked for TARGETS. Send an atom array back with the data
        // types we support.
        std::vector<XAtom> targets;
        targets.push_back(targets_atom);
        targets.push_back(save_targets_atom);
        targets.push_back(multiple_atom);
        RetrieveTargets(&targets);

        XChangeProperty(x_display_, requestor, property, XA_ATOM, 32,
            PropModeReplace,
            reinterpret_cast<unsigned char*>(&targets.front()),
            targets.size());
        return true;
    } else {
        // Try to find the data type in map.
        SelectionFormatMap::const_iterator it = format_map_.find(target);
        if (it != format_map_.end()) {
            if (it->second->size() > max_request_size_) {
                // We must send the data back in several chunks due to a limitation in
                // the size of X requests. Notify the selection requestor that the data
                // will be sent incrementally by returning data of type "INCR".
                long length = it->second->size();
                XChangeProperty(x_display_,
                    requestor,
                    property,
                    atom_cache_.GetAtom(kIncr),
                    32,
                    PropModeReplace,
                    reinterpret_cast<unsigned char*>(&length),
                    1);

                // Wait for the selection requestor to indicate that it has processed
                // the selection result before sending the first chunk of data. The
                // selection requestor indicates this by deleting |property|.
                base::TimeTicks timeout = base::TimeTicks::Now() + base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs);
                int foreign_window_manager_id = ui::XForeignWindowManager::GetInstance()->RequestEvents(
                    requestor, PropertyChangeMask);
                incremental_transfers_.push_back(
                    IncrementalTransfer(requestor,
                        target,
                        property,
                        it->second,
                        0,
                        timeout,
                        foreign_window_manager_id));

                // Start a timer to abort the data transfer in case that the selection
                // requestor does not support the INCR property or gets destroyed during
                // the data transfer.
                if (!incremental_transfer_abort_timer_.IsRunning()) {
                    incremental_transfer_abort_timer_.Start(
                        FROM_HERE,
                        base::TimeDelta::FromMilliseconds(kTimerPeriodMs),
                        this,
                        &SelectionOwner::AbortStaleIncrementalTransfers);
                }
            } else {
                XChangeProperty(
                    x_display_,
                    requestor,
                    property,
                    target,
                    8,
                    PropModeReplace,
                    const_cast<unsigned char*>(it->second->front()),
                    it->second->size());
            }
            return true;
        }
        // I would put error logging here, but GTK ignores TARGETS and spams us
        // looking for its own internal types.
    }
    return false;
}

void SelectionOwner::ProcessIncrementalTransfer(IncrementalTransfer* transfer)
{
    size_t remaining = transfer->data->size() - transfer->offset;
    size_t chunk_length = std::min(remaining, max_request_size_);
    XChangeProperty(
        x_display_,
        transfer->window,
        transfer->property,
        transfer->target,
        8,
        PropModeReplace,
        const_cast<unsigned char*>(transfer->data->front() + transfer->offset),
        chunk_length);
    transfer->offset += chunk_length;
    transfer->timeout = base::TimeTicks::Now() + base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs);

    // When offset == data->size(), we still need to transfer a zero-sized chunk
    // to notify the selection requestor that the transfer is complete. Clear
    // transfer->data once the zero-sized chunk is sent to indicate that state
    // related to this data transfer can be cleared.
    if (chunk_length == 0)
        transfer->data = NULL;
}

void SelectionOwner::AbortStaleIncrementalTransfers()
{
    base::TimeTicks now = base::TimeTicks::Now();
    for (int i = static_cast<int>(incremental_transfers_.size()) - 1;
         i >= 0; --i) {
        if (incremental_transfers_[i].timeout <= now)
            CompleteIncrementalTransfer(incremental_transfers_.begin() + i);
    }
}

void SelectionOwner::CompleteIncrementalTransfer(
    std::vector<IncrementalTransfer>::iterator it)
{
    ui::XForeignWindowManager::GetInstance()->CancelRequest(
        it->foreign_window_manager_id);
    incremental_transfers_.erase(it);

    if (incremental_transfers_.empty())
        incremental_transfer_abort_timer_.Stop();
}

std::vector<SelectionOwner::IncrementalTransfer>::iterator
SelectionOwner::FindIncrementalTransferForEvent(const XEvent& event)
{
    for (std::vector<IncrementalTransfer>::iterator it = incremental_transfers_.begin();
         it != incremental_transfers_.end();
         ++it) {
        if (it->window == event.xproperty.window && it->property == event.xproperty.atom) {
            return it;
        }
    }
    return incremental_transfers_.end();
}

SelectionOwner::IncrementalTransfer::IncrementalTransfer(
    XID window,
    XAtom target,
    XAtom property,
    const scoped_refptr<base::RefCountedMemory>& data,
    int offset,
    base::TimeTicks timeout,
    int foreign_window_manager_id)
    : window(window)
    , target(target)
    , property(property)
    , data(data)
    , offset(offset)
    , timeout(timeout)
    , foreign_window_manager_id(foreign_window_manager_id)
{
}

SelectionOwner::IncrementalTransfer::~IncrementalTransfer()
{
}

} // namespace ui
